Universal links in iOS are a great idea, and they are handled very elegantly. Apple came up with a great way to make this work in a trusted way. They even provide good tools to help debug when it isn't working.

And yet, during development on a recent project, our universal links just refused to work.

Let's dig into why, and why it took a while to find.

A quick intro to the steps. There's essentially two steps in enabling universal links:

  1. Enable the entitlement. This essentially tells the app "you can open links from this specific domain". Its done in XCode once, and forms part of your binary's meta-data.
  2. Provide a data file on the domain you want to enable links from. This is deployed to prove to the world that you, the domain owner approve of this app accepting your links.

This data file is called the Apple App Site Association file (often referred to as the AASA)

We won't go into detail of the contents here; Apple's documentation covers it well.

One thing to note is that the AASA is downloaded from Apple's content delivery network, which in turn retrieves it from the domain. And in our case, the client's development server would not be on the internet, so the CDN would not be able to retrieve it.

But, that's ok because apple have thought of that. When adding your applinks entitlement to your app you can specify ?mode=developer, and if your device is in development mode, it will retrieve it directly, bypassing the CDN. Great this is exactly what we need. The documentation states:

If you use a private web server, which is unreachable from the public internet, while developing your app, enable the alternate mode feature to bypass the CDN and connect directly to your server. To do this, add a query string to your associated domains entitlement

We had this switched on, so all good there.

Yet even when the device was on the VPN, and we attempted to click on our link, it always opened up the browser.

We started by using the tool on the device: Go to settings..developer..associated domains development..diagnostics. I pasted my URL, and it suggested it would work: Diagnostic output

So, we started digging further. We nearly got bitten by the fact that for historic reasons, our client's app had it's own App Prefix, which wasn't their Team Id. What? Yeah, that's another pitfall; but our friends over in Lickability have a whole post about this. I recommend it.

But that wasn't it either.

So, we went through all of the advice in Apple's excellent Technical Note on Debugging Universal links.

And yet, we were still struggling. The phone was on the VPN. I could access the AASA from the phone, but it was still failing.

So, we followed what felt like the last resort, and used sysdiagnose as outlined in the Tech Note. And sure enough, the swcutil_show.txt does have an entry for our app:

Service:              applinks
App ID:               <redacted, but yeah, our app identifier>
App Version:          311.0
App PI:               <LSPersistentIdentifier 0xaf0ea72e0> { v = 0, t = 0x8, u = 0xfac, db = A18CF074-C7D3-48C5-A133-ED65CB08C7CA, {length = 8, bytes = 0xac0f000000000000} }
Domain:               <client development domain>.com?mode=developer
User Approval:        unspecified
Site/Fmwk Approval:   unspecified
Flags:                developer
Last Checked:         2024-01-12 09:38:08 +0000
Next Check:           2024-01-12 12:45:37 +0000
Error:                Error Domain=HTTP Code=403 "(null)" UserInfo={Line=288, Function=-[SWCDownloader URLSession:dataTask:didReceiveResponse:completionHandler:]}
Retries:              1

But wait.

There's an error in there; It's a 403 denied.

We double checked again, and the file was downloadable using the browser on the device. We went back to the infrastructure team and asked about content blocking, or user agent checking. Maybe the file is accessible from the browser, but not other agents? Nope. It wasn't that.

And that got us thinking about when the file is downloaded. Is it when the link is tapped, or some other time? When else? Wait. What about at install time?

We removed the app from the device. We started the VPN on the device, and re-installed the app. We clicked on the link in the notes app...and bingo. It launched our app. It was that.

I couldn't find any mention in the documentation about when the AASA is retrieved, and I think when the developer mode was dreamt up, it was fair to assume that a server unreachable to the CDN, would be available to developers at all times during development; but we work with multiple clients, so we jump on and off different VPNs on an as-needed basis. In fact, it's common to be off a VPN until we need it, for network speed purposes alone.

Our problem was initially, we were installing the app, then starting the VPN, and then attempting to use the universal link.

So, if you run into a similar problem, and you think you've done everything from Apple's advice. This could be your cause: Make sure your AASA is available to download when installing your app.


Thanks for reading the Tapadoo blog. We've been building iOS and Android Apps since 2009. If your business needs an App, or you want advice on anything mobile, please get in touch